﻿using System;
using System.Reflection.Emit;
using Aeon.Emulator.DebugSupport;

namespace Aeon.Emulator.Decoding.Recompiler
{
    /// <summary>
    /// Generates linear addresses from 16-bit addressing modes.
    /// </summary>
    internal sealed class Addresser16
    {
        #region Private Fields
        private readonly RegisterExpressions registers;
        private ILGenerator il;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the Addresser16 class.
        /// </summary>
        /// <param name="registers">Associated register expression generator.</param>
        public Addresser16(RegisterExpressions registers)
        {
            this.registers = registers;
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Generates a 32-bit linear address from the provided operand.
        /// </summary>
        /// <param name="operand">Operand to generate linear address from.</param>
        /// <param name="prefixes">Prefixes which affect the instruction.</param>
        /// <param name="il">Associated ILGenerator instance.</param>
        public void GenerateLinearAddress(CodeOperand operand, PrefixState prefixes, ILGenerator il)
        {
            this.il = il;
            BuildFullAddress(operand, prefixes);
        }
        /// <summary>
        /// Generates the 16-bit offset of the address specified in the provided operand.
        /// </summary>
        /// <param name="operand">Operand to generate address offset from.</param>
        /// <param name="il">Associated ILGenerator instance.</param>
        public void GenerateOffset(CodeOperand operand, ILGenerator il)
        {
            this.il = il;
            BuildAddressOffset16(operand);
        }
        #endregion

        #region Private Static Methods
        /// <summary>
        /// Returns the default segment register for an operand.
        /// </summary>
        /// <param name="mode">Addressing mode of the operand.</param>
        /// <returns>Default segment register for the operand.</returns>
        private static string GetDefaultSegment16(CodeMemoryBase mode)
        {
            if(mode == CodeMemoryBase.BP || mode == CodeMemoryBase.BP_plus_DI || mode == CodeMemoryBase.BP_plus_SI)
                return "SS";
            else
                return "DS";
        }
        /// <summary>
        /// Returns the segment override register currently in effect.
        /// </summary>
        /// <param name="prefixes">Prefixes which affect the instruction.</param>
        /// <returns>Segment override register in effect if any; otherwise null.</returns>
        public static string GetOverrideSegment(PrefixState prefixes)
        {
            if((prefixes & PrefixState.CS) != 0)
                return "CS";
            else if((prefixes & PrefixState.DS) != 0)
                return "DS";
            else if((prefixes & PrefixState.SS) != 0)
                return "SS";
            else if((prefixes & PrefixState.ES) != 0)
                return "ES";
            else if((prefixes & PrefixState.FS) != 0)
                return "FS";
            else if((prefixes & PrefixState.GS) != 0)
                return "GS";
            else
                return null;
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Generates a 32-bit linear address from an operand.
        /// </summary>
        /// <param name="operand">Operand to get address of.</param>
        /// <param name="prefixes">Prefixes which affect the instruction.</param>
        private void BuildFullAddress(CodeOperand operand, PrefixState prefixes)
        {
            var segment = GetOverrideSegment(prefixes) ?? GetDefaultSegment16(operand.EffectiveAddress);

            this.registers.GenerateSegmentBaseValue(segment, il);
            BuildAddressOffset16(operand);
            il.Emit(OpCodes.Conv_U4);
            il.Emit(OpCodes.Add);
        }
        /// <summary>
        /// Generates the 16-bit offset of the address.
        /// </summary>
        /// <param name="operand">Operand to determine offset of.</param>
        private void BuildAddressOffset16(CodeOperand operand)
        {
            switch(operand.EffectiveAddress)
            {
            case CodeMemoryBase.BP:
                BuildAddOffset16("BP", (int)operand.ImmediateValue);
                break;

            case CodeMemoryBase.BP_plus_DI:
                BuildAddOffset16("BP", "DI", (int)operand.ImmediateValue);
                break;

            case CodeMemoryBase.BP_plus_SI:
                BuildAddOffset16("BP", "SI", (int)operand.ImmediateValue);
                break;

            case CodeMemoryBase.BX:
                BuildAddOffset16("BX", (int)operand.ImmediateValue);
                break;

            case CodeMemoryBase.BX_plus_DI:
                BuildAddOffset16("BX", "DI", (int)operand.ImmediateValue);
                break;

            case CodeMemoryBase.BX_plus_SI:
                BuildAddOffset16("BX", "SI", (int)operand.ImmediateValue);
                break;

            case CodeMemoryBase.DI:
                BuildAddOffset16("DI", (int)operand.ImmediateValue);
                break;

            case CodeMemoryBase.DisplacementOnly:
                il.LoadConstant((ushort)operand.ImmediateValue);
                il.Emit(OpCodes.Conv_U2);
                break;

            case CodeMemoryBase.SI:
                BuildAddOffset16("SI", (int)operand.ImmediateValue);
                break;

            default:
                throw new InvalidOperationException();
            }
        }
        /// <summary>
        /// Generates an offset by adding two registers and a displacement.
        /// </summary>
        /// <param name="r1">First register to add.</param>
        /// <param name="r2">Second register to add.</param>
        /// <param name="displacement">Displacement value to add.</param>
        private void BuildAddOffset16(string r1, string r2, int displacement)
        {
            this.registers.GenerateValue(r1, il);
            il.Emit(OpCodes.Conv_I2);
            this.registers.GenerateValue(r2, il);
            il.Emit(OpCodes.Conv_I2);
            il.Emit(OpCodes.Add);
            if(displacement != 0)
            {
                il.LoadConstant(displacement);
                il.Emit(OpCodes.Conv_I2);
                il.Emit(OpCodes.Add);
            }
            il.Emit(OpCodes.Conv_U2);
        }
        /// <summary>
        /// Generates an offset by adding a register and a displacement.
        /// </summary>
        /// <param name="r1">Register to add.</param>
        /// <param name="displacement">Displacemeht value to add.</param>
        private void BuildAddOffset16(string r1, int displacement)
        {
            this.registers.GenerateValue(r1, il);
            if(displacement != 0)
            {
                il.Emit(OpCodes.Conv_I2);
                il.LoadConstant(displacement);
                il.Emit(OpCodes.Conv_I2);
                il.Emit(OpCodes.Add);
                il.Emit(OpCodes.Conv_U2);
            }
        }
        #endregion
    }
}
